package cn.hftoday.core;

import java.util.*;
import java.lang.reflect.*;
import org.apache.log4j.*;
import cn.hftoday.db.*;
import cn.hftoday.util.*;

/**
 * Database mapper
 * @author lane.cn@gmail.com
 *
 */
public class Mapper {
	private static final Logger LOG = Logger.getLogger(Mapper.class);
	private static Mapper _instance = null;
	
	private Map<String, Map<String, String>> statements = new HashMap<String, Map<String, String>>();
	
	private Map<String, Class> classes = new HashMap<String, Class>();
	private Map<String, Method> methods = new HashMap<String, Method>();
	private Map<String, Constructor> constructors = new HashMap<String, Constructor>();
	private Map<String, TypeHandler> typeHandlers = new HashMap<String, TypeHandler>();
	
	
	/**
	 * private constructor
	 */
	private Mapper() {
	}
	
	/**
	 * get static instance
	 * @return
	 */
	public static Mapper getInstance() {
		if (_instance == null) {
			_instance = new Mapper();
			_instance.init();
		}
		return _instance;
	}
	
	/**
	 * query database for single object
	 * @param statement
	 * @param args
	 * @return
	 */
	public Object queryForObject(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_select")) {
			throw new RuntimeException("error.mapper.not.select");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		RecordSet rs = ConnectionPool.getInstance().getConnection("sl").select(sql);
		if (rs.getRowCount() == 0) {
			return null;
		}
		
		Object object = null;
		try {
			Constructor con = getStatementConstructor(rs, 0, statement);
			Class c = getStatementClass(rs, 0, statement);
			Method m = getStatementMethod(rs, 0, statement);
			object = con.newInstance();
			Object[] params = new Object[] {object, rs, 0};
			m.invoke(c, params);
		}
		catch (Exception e) {
			LOG.error("cannot execute query statement", e);
			throw new RuntimeException("error.mapper.query.object", e);
		}
		return object;
	}
	
	/**
	 * query database for properties map
	 * @param statement
	 * @param args
	 * @return
	 */
	public Map<String, String> queryForMap(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_select")) {
			throw new RuntimeException("error.mapper.not.select");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		RecordSet rs = ConnectionPool.getInstance().getConnection("sl").select(sql);
		if (rs.getRowCount() == 0) {
			return null;
		}
		return rs.getRowList().get(0);
	}

	/**
	 * query database for object list
	 * @param statement
	 * @param args
	 * @return
	 */
	public List queryForList(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_select")) {
			throw new RuntimeException("error.mapper.not.select");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		RecordSet rs = ConnectionPool.getInstance().getConnection("sl").select(sql);
		
		List ary = new ArrayList();
		try {
			for (int i = 0; i < rs.getRowCount(); i ++) {
				Constructor con = getStatementConstructor(rs, i, statement);
				if (con == null) {
					return rs.getRowList();
				}
				Class c = getStatementClass(rs, i, statement);
				Method m = getStatementMethod(rs, i, statement);
				Object object = con.newInstance();
				Object[] params = new Object[] {object, rs, i};
				m.invoke(c, params);
				ary.add(object);
			}
		}
		catch (Exception e) {
			LOG.error("cannot execute query list statement", e);
			throw new RuntimeException("error.mapper.query.list", e);
		}
		return ary;
	}
	
	/**
	 * insert object into database
	 * @param statement
	 * @param object
	 * @return
	 */
	public Object insert(String statement, Object object) {
		if (!getStatementMapper(statement).equals("_insert")) {
			throw new RuntimeException("error.mapper.not.insert");
		}
		
		String sql = getStatementSql(statement);
		Class c = getStatementClass(null, 0, statement);
		Method m = getStatementMethod(null, 0, statement);
		
		Object[] args = new Object[] {object, sql};
		Object ret = null;
		try {
			ret = m.invoke(c, args); 
		}
		catch (Exception e) {
			LOG.error("cannot execute insert statement", e);
			throw new RuntimeException("error.mapper.insert", e);
		}
		ConnectionPool.getInstance().getConnection("sl").update(ret.toString());
		return object;
	}
	
	/**
	 * insert strings into database
	 * @param statement
	 * @param args
	 * @return
	 */
	public int insert(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_insert")) {
			throw new RuntimeException("error.mapper.not.insert");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		return ConnectionPool.getInstance().getConnection("sl").update(sql);
	}
	
	/**
	 * update object in database
	 * @param statement
	 * @param object
	 * @return
	 */
	public int update(String statement, Object object) {
		if (!getStatementMapper(statement).equals("_update")) {
			throw new RuntimeException("error.mapper.not.update");
		}
		
		String sql = getStatementSql(statement);
		Class c = getStatementClass(null, 0, statement);
		Method m = getStatementMethod(null, 0, statement);
		
		Object[] args = new Object[] {object, sql};
		Object ret = null;
		try {
			ret = m.invoke(c, args); 
		}
		catch (Exception e) {
			LOG.error("cannot execute update statement", e);
			throw new RuntimeException("error.mapper.update", e);
		}
		return ConnectionPool.getInstance().getConnection("sl").update(ret.toString());
	}
	
	/**
	 * update string in database
	 * @param statement
	 * @param args
	 * @return
	 */
	public int update(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_update")) {
			throw new RuntimeException("error.mapper.not.update");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		return ConnectionPool.getInstance().getConnection("sl").update(sql);
	}
	
	/**
	 * delete object in database
	 * @param statement
	 * @param object
	 * @return
	 */
	public int delete(String statement, Object object) {
		if (!getStatementMapper(statement).equals("_delete")) {
			throw new RuntimeException("error.mapper.not.delete");
		}
		
		String sql = getStatementSql(statement);
		Class c = getStatementClass(null, 0, statement);
		Method m = getStatementMethod(null, 0, statement);
		
		Object[] args = new Object[] {object, sql};
		Object ret = null;
		try {
			ret = m.invoke(c, args); 
		}
		catch (Exception e) {
			LOG.error("cannot execute delete statement", e);
			throw new RuntimeException("error.mapper.delete", e);
		}
		return ConnectionPool.getInstance().getConnection("sl").update(ret.toString());
	}
	
	/**
	 * delete string in database
	 * @param statement
	 * @param args
	 * @return
	 */
	public int delete(String statement, String ...args) {
		if (!getStatementMapper(statement).equals("_delete")) {
			throw new RuntimeException("error.mapper.not.delete");
		}
		
		String sql = getStatementSql(statement);
		sql = StringParser.formatSql(sql, args);
		return ConnectionPool.getInstance().getConnection("sl").update(sql);
	}
	
	/**
	 * initialize maps
	 */
	private void init() {
		LOG.info("initialize object/database mapper");
		RecordSet rs = ConnectionPool.getInstance().getConnection("sl").select("select * from t_sqlstatement");
		
		for (int i = 0; i < rs.getRowCount(); i ++) {
			String id = rs.getItem(i, "f_id");
			statements.put(id, rs.getRowList().get(i));
		}
	}
	
	/**
	 * get sql statement
	 * @param id
	 * @return
	 */
	private String getStatementSql(String id) {
		return statements.get(id).get("f_sql").replace('\r', ' ').replace('\n', ' ');
	}
	
	/**
	 * get mapper name
	 * @param id
	 * @return
	 */
	private String getStatementMapper(String id) {
		return statements.get(id).get("f_mapper");
	}
	
	/**
	 * get class by statement name
	 * @param id
	 * @param nouse
	 * @return
	 */
	private String getStatementClass(RecordSet rs, int index, String id, int nouse) {
		String clazz = statements.get(id).get("f_class");
		//LOG.debug("clazz: " + clazz);
		if (clazz != null && !clazz.trim().equals("")) {
			return clazz;
		}
		
		String discriminatior = statements.get(id).get("f_discriminator");
		//LOG.debug("discriminatior: " + discriminatior);
		if (discriminatior == null || discriminatior.trim().equals("")) {
			return null;
		}
		
		String typehandler = statements.get(id).get("f_typehandler");
		//LOG.debug("typehandler: " + typehandler);
		if (typehandler == null || typehandler.trim().equals("")) {
			return discriminatior;
		}
		
		//create type handler instance
		if (!typeHandlers.containsKey(typehandler)) {
			try {
				Class c = Class.forName(typehandler);
				Object o = c.newInstance();
				typeHandlers.put(typehandler, (TypeHandler)o);
			}
			catch (Exception e) {
				LOG.fatal("cannot initialize object/database mapper", e);
				throw new RuntimeException("error.init.mapper", e);
			}
		}
		discriminatior = rs.getItem(index, discriminatior);
		return typeHandlers.get(typehandler).getResult(discriminatior);
	}
	
	/**
	 * get class by statement name
	 * @param id
	 * @return
	 */
	private Class getStatementClass(RecordSet rs, int index, String id) {
		String clazz = getStatementClass(rs, index, id, 0);
		//LOG.debug("***** get class name: " + clazz);
		if (clazz == null || clazz.trim().equals("")) {
			return null;
		}
		
		if (classes.containsKey(clazz)) {
			return classes.get(clazz);
		}
		
		try {
			Class c = Class.forName(clazz);
			classes.put(clazz, c);
		}
		catch (Exception e) {
			LOG.fatal("cannot initialize object/database mapper", e);
			throw new RuntimeException("error.init.mapper", e);
		}
		return classes.get(clazz);
	}
	
	/**
	 * get class constructor by statement name
	 * @param id
	 * @return
	 */
	private Constructor getStatementConstructor(RecordSet rs, int index, String id) {
		Class c = getStatementClass(rs, index, id);
		if (c == null) {
			return null;
		}
		
		String clazz = getStatementClass(rs, index, id, 0);
		if (constructors.containsKey(clazz)) {
			return constructors.get(clazz);
		}
		
		try {
			Class[] types = new Class[0];
			Constructor constructor = c.getDeclaredConstructor(types);
			constructor.setAccessible(true);
			constructors.put(clazz, constructor);
		}
		catch (Exception e) {
			LOG.fatal("cannot initialize object/database mapper", e);
			throw new RuntimeException("error.init.mapper", e);
		}
		return constructors.get(clazz);
	}
	
	/**
	 * get method by statement name
	 * @param id
	 * @return
	 */
	private Method getStatementMethod(RecordSet rs, int index, String id) {
		Class c = getStatementClass(rs, index, id);
		if (c == null) {
			return null;
		}
		
		String clazz = getStatementClass(rs, index, id, 0);
		String mapper = statements.get(id).get("f_mapper");
		String method = clazz + "." + mapper;
		if (methods.containsKey(method)) {
			return methods.get(method);
		}
		
		try {
			Class[] types = null;
			if (mapper.equals("_select")) {
				types = new Class[] {Object.class, RecordSet.class, int.class};
			}
			else {
				types = new Class[] {Object.class, String.class};
			}
			Method m = c.getDeclaredMethod(mapper, types);
			m.setAccessible(true);
			methods.put(method, m);
		}
		catch (Exception e) {
			LOG.fatal("cannot initialize object/database mapper", e);
			throw new RuntimeException("error.init.mapper", e);
		}
		return methods.get(method);
	}
}
